import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;

/**
 * 访问服务器的代码
 */
public class CloudShare {
    private static final int MAX_INPUT_SIZE = 28000;
    private static final int MAX_OUTPUT_SIZE = 28000;
    private static final int SLEEP_MILLIS = (int) (0.2 * 1000);
    private static final int CONNECT_TIMEOUT = 5 * 1000;
    private static final int WAIT_RESPONSE_TIMEOUT = 5 * 1000;

    private static final String OK_STRING = "OK";
    private static final String HEAD_STRING = "Santoker Share v0.1";
    private static final byte[] HEAD_BYTES = "Santoker Share v0.1\n".getBytes();
    private static final byte[] GET_HEAD_BYTES = "Santoker Share v0.1\nGet\n".getBytes();
    private static final byte[] SHARE_HEAD_BYTES = "Santoker Share v0.1\nShare\n".getBytes();
    private static final byte[] REQUEST_END_BYTES = "Request End\n".getBytes();
    private static final byte[] RESPONSE_END_BYTES = "Response End\n".getBytes();

    private final int port;
    private final String address;

    public CloudShare(String address, int port) {
        this.port = port;
        this.address = address;
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException ignored) {
        }
    }

    private String[] doStringsResponse(byte[] data, int length) throws ShareException {
        int base = length - RESPONSE_END_BYTES.length;
        if (base < 0) return null;

        for (int i = 0; i < RESPONSE_END_BYTES.length; ++i)
            if (RESPONSE_END_BYTES[i] != data[base + i])
                return null;

        String[] response = (new String(data, 0, length)).split("(?<!\\\\)\\n");
        if (response.length < 3)
            throw new ShareException("Response data error");
        if (!HEAD_STRING.equals(response[0]))
            throw new ShareException("Version mismatch");
        if (!OK_STRING.equals(response[1]))
            throw new ShareException(response[1]);

        String[] results = new String[response.length - 3];
        for (int i = 0; i < results.length; i++)
            results[i] = response[2 + i];
        return results;

    }

    private String doStringResponse(byte[] data, int length) throws ShareException {
        String[] strings = doStringsResponse(data, length);

        if (strings == null)
            return null;
        if (strings.length != 1)
            throw new ShareException("Response data error");
        return strings[0];
    }

    private byte[] doGetResponse(byte[] data, int length) throws ShareException {
        int flag = 0, begin = 0, end = 0;
        for (int i = 0; i < length && flag < 3; ++i) {
            if (data[i] == '\n') {
                ++flag;
                begin = end;
                end = i + 1;

                String str = new String(data, begin, end - begin - 1);
                if (flag == 1) {
                    if (!HEAD_STRING.equals(str))
                        throw new ShareException("Version mismatch");
                } else if (flag == 2) {
                    if (!OK_STRING.equals(str))
                        throw new ShareException(str);
                } else if (flag == 3) {
                    try {
                        int dataLength = Integer.valueOf(str);
                        if (length - end < dataLength)
                            return null;
                        if (length - end > dataLength)
                            throw new ShareException("Response data error");
                    } catch (NumberFormatException e) {
                        throw new ShareException("Response data error", e);
                    }
                }
            }
        }

        byte[] d = new byte[length - end];
        System.arraycopy(data, end, d, 0, length - end);
        return d;
    }

    public String share(final byte[] data) throws ShareException {
        if (data.length >= MAX_OUTPUT_SIZE)
            throw new ShareException("Shared data is too big");

        try (NetworkConnector connector = new NetworkConnector()) {
            // send data
            OutputStream out = connector.getOutputStream();
            out.write(SHARE_HEAD_BYTES);
            out.write((data.length + "\n").getBytes());
            out.write(data);

            // get result
            // wait up to {@link CloudShare#WAIT_RESPONSE_TIMEOUT} secs
            int index = 0;
            long millis = System.currentTimeMillis();
            byte[] buffer = new byte[MAX_INPUT_SIZE];
            InputStream in = connector.getInputStream();
            while (true) {
                if (in.available() > 0) {
                    index += in.read(buffer, index, MAX_INPUT_SIZE - index);

                    String response = doStringResponse(buffer, index);
                    if (response != null)
                        return response;
                    if (index == MAX_INPUT_SIZE)
                        throw new ShareException("Response data too big");
                }

                if (System.currentTimeMillis() - millis >= WAIT_RESPONSE_TIMEOUT)
                    throw new ShareException("Timeout");
                if (connector.isInputShutdown())
                    throw new ShareException("Connection closed");
                sleep(SLEEP_MILLIS);
            }
        } catch (IOException e) {
            throw new ShareException(e);
        }
    }

    public byte[] get(String str) throws ShareException {
        try (NetworkConnector connector = new NetworkConnector()) {
            // send request
            OutputStream out = connector.getOutputStream();
            out.write(GET_HEAD_BYTES);
            out.write((str + "\n").getBytes());
            out.write(REQUEST_END_BYTES);

            // get result
            // wait up to {@link CloudShare#WAIT_RESPONSE_TIMEOUT} secs
            int index = 0;
            long millis = System.currentTimeMillis();
            byte[] buffer = new byte[MAX_INPUT_SIZE];
            InputStream in = connector.getInputStream();
            while (true) {
                if (in.available() > 0) {
                    index += in.read(buffer, index, MAX_INPUT_SIZE - index);

                    byte[] response = doGetResponse(buffer, index);
                    if (response != null)
                        return response;
                    if (index == MAX_INPUT_SIZE)
                        throw new ShareException("Response data too big");
                }

                if (System.currentTimeMillis() - millis >= WAIT_RESPONSE_TIMEOUT)
                    throw new ShareException("Timeout");
                if (connector.isInputShutdown())
                    throw new ShareException("Connection closed");
                sleep(SLEEP_MILLIS);
            }
        } catch (IOException e) {
            throw new ShareException(e);
        }
    }

    public String statistic() throws ShareException {
        try (NetworkConnector connector = new NetworkConnector()) {
            // send data
            OutputStream out = connector.getOutputStream();
            out.write(HEAD_BYTES);
            out.write("Statistic\n".getBytes());
            out.write(REQUEST_END_BYTES);

            // get result
            // wait up to {@link CloudShare#WAIT_RESPONSE_TIMEOUT} secs
            int index = 0;
            long millis = System.currentTimeMillis();
            byte[] buffer = new byte[MAX_INPUT_SIZE];
            InputStream in = connector.getInputStream();
            while (true) {
                if (in.available() > 0) {
                    index += in.read(buffer, index, MAX_INPUT_SIZE - index);

                    String response = doStringResponse(buffer, index);
                    if (response != null)
                        return response;
                    if (index == MAX_INPUT_SIZE)
                        throw new ShareException("Response data too big");
                }

                if (System.currentTimeMillis() - millis >= WAIT_RESPONSE_TIMEOUT)
                    throw new ShareException("Timeout");
                if (connector.isInputShutdown())
                    throw new ShareException("Connection closed");
                sleep(SLEEP_MILLIS);
            }
        } catch (IOException e) {
            throw new ShareException(e);
        }
    }

    public String save() throws ShareException {
        try (NetworkConnector connector = new NetworkConnector()) {
            // send data
            OutputStream out = connector.getOutputStream();
            out.write(HEAD_BYTES);
            out.write("Save\n".getBytes());
            out.write(REQUEST_END_BYTES);

            int index = 0;
            long millis = System.currentTimeMillis();
            byte[] buffer = new byte[MAX_INPUT_SIZE];
            InputStream in = connector.getInputStream();
            while (true) {
                if (in.available() > 0) {
                    index += in.read(buffer, index, MAX_INPUT_SIZE - index);

                    String response = doStringResponse(buffer, index);
                    if (response != null)
                        return response;
                    if (index == MAX_INPUT_SIZE)
                        throw new ShareException("Response data too big");
                }

                if (System.currentTimeMillis() - millis >= WAIT_RESPONSE_TIMEOUT)
                    throw new ShareException("Timeout");
                if (connector.isInputShutdown())
                    throw new ShareException("Connection closed");
                sleep(SLEEP_MILLIS);
            }
        } catch (IOException e) {
            throw new ShareException(e);
        }
    }

    public String[] keys() throws ShareException {
        try (NetworkConnector connector = new NetworkConnector()) {
            // send data
            OutputStream out = connector.getOutputStream();
            out.write(HEAD_BYTES);
            out.write("Keys\n".getBytes());
            out.write(REQUEST_END_BYTES);

            int index = 0;
            long millis = System.currentTimeMillis();
            byte[] buffer = new byte[MAX_INPUT_SIZE];
            InputStream in = connector.getInputStream();
            while (true) {
                if (in.available() > 0) {
                    index += in.read(buffer, index, MAX_INPUT_SIZE - index);

                    String[] response = doStringsResponse(buffer, index);
                    if (response != null)
                        return response;
                    if (index == MAX_INPUT_SIZE)
                        throw new ShareException("Response data too big");
                }

                if (System.currentTimeMillis() - millis >= WAIT_RESPONSE_TIMEOUT)
                    throw new ShareException("Timeout");
                if (connector.isInputShutdown())
                    throw new ShareException("Connection closed");
                sleep(SLEEP_MILLIS);
            }
        } catch (IOException e) {
            throw new ShareException(e);
        }
    }

    class NetworkConnector implements Closeable {
        final Socket socket;

        NetworkConnector() throws IOException {
            socket = new Socket();
            SocketAddress socketAddress = new InetSocketAddress(address, port);
            socket.connect(socketAddress, CONNECT_TIMEOUT);
        }

        public void close() throws IOException {
            socket.close();
        }

        boolean isClosed() {
            return socket.isClosed();
        }

        boolean isInputShutdown() {
            return socket.isInputShutdown();
        }

        boolean isOutputShutdown() {
            return socket.isOutputShutdown();
        }

        InputStream getInputStream() throws IOException {
            return socket.getInputStream();
        }

        OutputStream getOutputStream() throws IOException {
            return socket.getOutputStream();
        }
    }
}
